package com.google.bitcoin.core;

import com.google.bitcoin.store.BlockStoreException;
import com.google.bitcoin.store.JdbcDiskBlockStore;
import java.math.BigInteger;
import java.util.List;
import java.util.logging.Level;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Validator {

    private static final Logger log = LoggerFactory.getLogger(Validator.class);

    static void verifyBlockTransactions(JdbcDiskBlockStore blockStore, Block block) throws VerificationException, BlockStoreException {
        if (block.getTransactions().size() > 1) {
            log.debug("Verifying block " + block.getHashAsString());
        }
        try {
            for (int i = 1; i < block.getTransactions().size(); i++) {
                verifyTransaction(blockStore, block.getTransactions().get(i));
            }
        } catch (VerificationException e) {
            log.error(e.getMessage());
            throw e;
        }
    }

    // Standard payment type (pubkey-hash): DUP HASH160 <hash of public key> EQUALVERIFY CHECKSIG
    // “hash of public key” is what a bitcoin address represents.
    // An address is the human readable form, whereas pubkey-hash is the computer readable form.
    // bool is_pubkey_hash_type(const operation_stack& ops) {
    //   return ops.size() == 5 &&
    //       ops[0].code == opcode::dup &&
    //       ops[1].code == opcode::hash160 &&
    //       ops[2].code == opcode::special &&
    //       ops[3].code == opcode::equalverify &&
    //       ops[4].code == opcode::checksig;
    //}
    /**
     * Return true if the transaction is valid, false if orphan. Throws
     * VerificationException if invalid
     *
     * @param blockStore
     * @param tx
     * @return
     * @throws VerificationException
     * @throws BlockStoreException
     */
    // Implement checks defined here: https://en.bitcoin.it/wiki/Protocol_rules#cite_ref-3
    // TODO: Check if useful to define a new kind of exception for every possible verification exception
    private static boolean verifyTransaction(JdbcDiskBlockStore blockStore, Transaction tx) throws VerificationException, BlockStoreException {
        assert (!tx.isCoinBase());
        log.debug(" Verifying tx " + tx.getHashAsString());

        // #2: Make sure neither in or out lists are empty
        if (tx.getInputs().isEmpty() || tx.getOutputs().isEmpty()) {
            throw new VerificationException("Zero number of inputs or outputs in tx " + tx.getHashAsString());
        }

        BigInteger totalIn = new BigInteger("0");
        for (TransactionInput txIn : tx.getInputs()) {
            TransactionOutPoint outPoint = txIn.getOutpoint();
            try {
                //txIn.parse();
                log.debug("  txIn: " + txIn + " sourceTx: " + outPoint.getHash() + " sourceIndex: " + outPoint.getIndex());
                //} catch (ProtocolException e) {
                //log.debug("  -- ProtocolException in parse txIn -- sourceTx: " + outPoint.getHash() + " sourceIndex: " + outPoint.getIndex());
            } catch (RuntimeException e) {
                log.debug("  -- errore in parse o out txIn -- sourceTx: " + outPoint.getHash() + " sourceIndex: " + outPoint.getIndex());
            }

            // #5: Check that hashcode is not 0 (coinbase transaction)
            if (outPoint.getHash().equals(Sha256Hash.ZERO_HASH)) {
                throw new VerificationException("TxInput refers to Coinbase Tx");
            }

            // #10: If source transaction not found that's an orphan Tx
            Transaction sourceTx = blockStore.getTransaction(outPoint.getHash());
            if (sourceTx == null) {
                return false;
            }
            log.debug("  sourceTx found -- numOutputs: " + sourceTx.getOutputs().size() + " isCoinBase: " + sourceTx.isCoinBase());
            try {
                log.debug("   sourceTx: " + sourceTx);
            } catch (RuntimeException e) {
                log.debug("  -- RTE in output di sourceTx");
            }

            // #11: Check that referenced transaction index exists
            if (sourceTx.getOutputs().size() < outPoint.getIndex()) {
                throw new VerificationException("Referenced output #" + outPoint.getIndex() + " but Tx " + outPoint.getHash() + " has only " + sourceTx.getOutputs().size() + " outputs");
            }

            // #8: Check that sourceTx is not already in main chain
            if (!blockStore.isTxInMainChain(outPoint.getHash())) {
                throw new VerificationException("Tx not found in main chain: " + outPoint.getHash());
            }

            // #14 Check that txout not redeemed yet
            if (blockStore.isTxInputRedeemedInMainChain(outPoint.getHash(), outPoint.getIndex())) {
                throw new VerificationException("TxInput #" + outPoint.getIndex() + " of transaction " + outPoint.getHash() + " already redeemed in main chain");
            }

            // TODO: #13: Verfy signatures
            TransactionOutput sourceOut = sourceTx.getOutputs().get((int) outPoint.getIndex());
            try {
                TransactionInput sourceTxFirstInput = sourceTx.getInputs().get(0);
                Script outScript = txIn.getScriptSig();
                Script inScript = sourceOut.getScriptPubKey();
                List<byte[]> outChunks = outScript.getChunks();
                List<byte[]> inChunks = inScript.getChunks();
                log.debug("  sourceTxFirstInput: " + sourceTxFirstInput);
                log.debug("  outScript/txIn: " + outScript + " pubKey: " + Utils.bytesToHexString(outScript.getPubKey()) +
                        " numChunks: " + (outChunks == null ? "NONE" : outChunks.size()) + " isSentToIp: " + outScript.isSentToIP());
                if (outScript.isSentToIP()) {
                    log.debug("   outScript.decodedAddr: " + new VersionedChecksummedBytes(0, Utils.sha256hash160(outScript.getChunks().get(0))));
                }
                log.debug("  inScript/sourceOut: " + inScript + " pubKey: " + Utils.bytesToHexString(inScript.getPubKey()) +
                        " numChunks: " + (inChunks == null ? "NONE" : inChunks.size()) + " isSentToIp: " + inScript.isSentToIP());
                if (inScript.isSentToIP()) {
                    log.debug("   inScript.decodedAddr: " + new VersionedChecksummedBytes(0, Utils.sha256hash160(inScript.getChunks().get(0))));
                }
                /*
                 * log.debug("sourceTxFirstInput: " + sourceTxFirstInput + //"
                 * outScript/txIn.pubKey: " + outScript.getPubKey() + "
                 * outScript.pubKeyHash: " + outScript.getPubKeyHash() + "
                 * inScript/sourceOut.pubKey: " + inScript.getPubKey() //"
                 * inScript.pubKeyHash: " + inScript.getPubKeyHash() );
                 */
            } catch (ScriptException e) {
                log.debug("  -- ScriptException in verifica signature: " + e.getMessage(), e);
            } catch (RuntimeException e) {
                log.debug("  -- RTE in verifica signature");
            }

            // TODO: #9: Check if any other tx in the pool uses the same transaction output as one used by this tx

            totalIn = totalIn.add(sourceOut.getValue());
            if (sourceOut.getMessageSize() == 5) {
                try {
                    log.debug("  sourceOut: " + sourceOut + " standard PubKey: " + sourceOut.getScriptPubKey() + " totalIn: " + totalIn);
                } catch (ScriptException ex) {
                    log.debug("  ERROR WHILE PARSING PubKey of sourceOut");
                    java.util.logging.Logger.getLogger(Validator.class.getName()).log(Level.SEVERE, null, ex);
                }
            } else {
                log.debug("  sourceOut value: " + sourceOut.getValue() + " messageSize: " + sourceOut.getMessageSize() + " totalIn: " + totalIn);
            }
        }

        BigInteger totalOut = new BigInteger("0");
        for (TransactionOutput txOut : tx.getOutputs()) {
            totalOut = totalOut.add(txOut.getValue());
            // toString fails for transaction 11ce216310a02cb4c7ee77fd079479fe64b710f05d3d67ad8f376ad7cf20f34c
            // log.debug("  txOut: " + txOut + " totalOut: " + totalOut);
        }

        if (totalOut.compareTo(totalIn) > 0) {
            throw new VerificationException("TotalOut " + totalOut + " greater than totalInput: " + totalIn + " in tx " + tx.getHashAsString());
        }
        return true;
    }
}
/*
 TRUNCATE TABLE  TxOutput; 
 TRUNCATE TABLE  TxInput; 
 TRUNCATE TABLE  Transaction;
 TRUNCATE TABLE Counter;
 TRUNCATE TABLE Chain;
 TRUNCATE TABLE BlockTx;
 TRUNCATE TABLE Block;
 */